package com.atguigu.gulimall.search.service.impl;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.json.JSONUtil;
import com.alibaba.nacos.common.utils.CollectionUtils;
import com.atguigu.common.constant.SearchConstant;
import com.atguigu.common.to.SkuEsModel;
import com.atguigu.common.utils.R;
import com.atguigu.gulimall.search.config.EsConfig;
import com.atguigu.gulimall.search.feign.ProductFeignService;
import com.atguigu.gulimall.search.service.MallSearchService;
import com.atguigu.gulimall.search.vo.*;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.NestedQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.MultiBucketsAggregation;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @create: 2024/9/1
 * @author: gyhe10
 * @description:
 **/

@Service
public class MallSearchServiceImpl implements MallSearchService {

    @Resource
    private RestHighLevelClient restHighLevelClient;
    @Resource
    private ProductFeignService productFeignService;

    @Override
    public SearchResult search(SearchParam param) {

        // 构建查询请求，动态构建查询 DSL 语句
        SearchRequest searchRequest = buildSearchRequest(param);

        SearchResult searchResult = null;
        try {
            // 执行查询
            SearchResponse response = restHighLevelClient.search(searchRequest, EsConfig.COMMON_OPTIONS);
            // 解析响应结果，封装为 SearchResult
            searchResult = buildSearchResult(response, param);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }

        return searchResult;
    }


    /**
     * 构建查询请求
     *
     * @param param
     * @return
     */
    private SearchRequest buildSearchRequest(SearchParam param) {

        SearchSourceBuilder builder = new SearchSourceBuilder();

        // 使用 builder 构建查询 DSL 语句
        // 构建 bool 查询
        BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
        // 模糊检索
        if (StringUtils.hasText(param.getKeyword())) {
            boolQuery.must(QueryBuilders.matchQuery("skuTitle", param.getKeyword()));
        }
        // 三级分类过滤
        if (param.getCatalog3Id() != null) {
            boolQuery.filter(QueryBuilders.termQuery("catalogId", param.getCatalog3Id()));
        }
        // 品牌过滤
        if (CollectionUtil.isNotEmpty(param.getBrandId())) {
            boolQuery.filter(QueryBuilders.termsQuery("brandId", param.getBrandId()));
        }
        // 属性过滤
        if (CollectionUtil.isNotEmpty(param.getAttrs())) {
            // attrs: attr1=6_8G:16G&attr2=13_14寸:15.6寸
            for (String attr : param.getAttrs()) {
                // 每个过滤属性，都得生成一个 nestedQuery
                BoolQueryBuilder nestedBoolQuery = QueryBuilders.boolQuery();
                String[] s = attr.split("_");
                String attrId = s[0];
                String[] attrValues = s[1].split(":");
                nestedBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", attrId));
                nestedBoolQuery.must(QueryBuilders.termsQuery("attrs.attrValue", attrValues));
                /**
                 * nestedQuery() 三个参数：
                 *      路径（Path）：指定嵌套对象的路径。这是必须的参数，用于告诉 Elasticsearch 查询哪个嵌套对象
                 *      子查询（Query）：指定要应用于嵌套对象的子查询。这可以是任何有效的 Elasticsearch 查询
                 *      评分模式（Score Mode）：指定评分模式，用于确定如何计算嵌套对象的评分。这是一个可选参数，默认值为 none。
                 */
                NestedQueryBuilder nestedQuery = QueryBuilders.nestedQuery("attrs", nestedBoolQuery, ScoreMode.None);
                boolQuery.filter(nestedQuery);
            }
        }
        // 是否有库存过滤
        if (param.getHasStock() != null) {
            boolQuery.filter(QueryBuilders.termQuery("hasStock", param.getHasStock() == 1));
        }
        // 价格区间过滤
        if (StringUtils.hasText(param.getSkuPrice())) {
            String[] s = param.getSkuPrice().split("_");
            if (s.length == 2) {
                // 区间查询
                boolQuery.filter(QueryBuilders.rangeQuery("skuPrice").gte(s[0]).lte(s[1]));
            } else if (s.length == 1) {
                // 只有一个值
                if (param.getSkuPrice().startsWith("_")) {
                    // _5000，小于等于 5000
                    boolQuery.filter(QueryBuilders.rangeQuery("skuPrice").lte(s[0]));
                } else {
                    // 5000_，大于等于 5000
                    boolQuery.filter(QueryBuilders.rangeQuery("skuPrice").gte(s[0]));
                }
            }
        }
        // 构建查询请求
        builder.query(boolQuery);

        // 排序
        if (StringUtils.hasText(param.getSort())) {
            // sort=saleCount_desc
            String[] s = param.getSort().split("_");
            builder.sort(s[0], s[1].equalsIgnoreCase("asc") ? SortOrder.ASC : SortOrder.DESC);
        }

        // 分页
        // from = (pageNum - 1) * pageSize
        builder.from((param.getPageNum() - 1) * SearchConstant.PRODUCT_PAGE_SIZE);
        builder.size(SearchConstant.PRODUCT_PAGE_SIZE);

        // 高亮
        if (StringUtils.hasText(param.getKeyword())) {
            builder.highlighter(new HighlightBuilder().field("skuTitle").preTags("<b style='color:red'>").postTags("</b>"));
        }

        // 聚合
        // 聚合品牌，检索结果涉及的品牌、品牌名称、品牌logo图片
        builder.aggregation(AggregationBuilders.terms("brand_agg").field("brandId").size(50)
                // 子聚合，聚合品牌名称
                .subAggregation(AggregationBuilders.terms("brand_name_agg").field("brandName").size(1))
                // 子聚合，聚合品牌图片
                .subAggregation(AggregationBuilders.terms("brand_img_agg").field("brandImg").size(1)));
        // 聚合分类，检索结果涉及的分类、分类名称、分类图标
        builder.aggregation(AggregationBuilders.terms("catalog_agg").field("catalogId").size(20)
                // 子聚合，聚合分类名称
                .subAggregation(AggregationBuilders.terms("catalog_name_agg").field("catalogName").size(1))
                // 子聚合，聚合分类图标
                .subAggregation(AggregationBuilders.terms("catalog_img_agg").field("catalogImg").size(1)));
        // 聚合属性，检索结果涉及的属性、属性名、属性值
        // 属性 attrs 是一个 nested 类型，所以需要使用嵌入式聚合
        builder.aggregation(AggregationBuilders.nested("attr_agg", "attrs")
                // 聚合属性 id
                .subAggregation(AggregationBuilders.terms("attr_id_agg").field("attrs.attrId")
                        // 子聚合，聚合属性名
                        .subAggregation(AggregationBuilders.terms("attr_name_agg").field("attrs.attrName").size(1))
                        // 子聚合，聚合属性值
                        .subAggregation(AggregationBuilders.terms("attr_value_agg").field("attrs.attrValue").size(50))));

        // 打印 DSL
        // String dslStr = builder.toString();
        // System.out.println("dslStr = " + dslStr);

        return new SearchRequest(new String[]{SearchConstant.PRODUCT_INDEX}, builder);
    }

    /**
     * 解析响应结果，封装为 SearchResult
     *
     * @param response
     * @return
     */
    private SearchResult buildSearchResult(SearchResponse response, SearchParam param) {
        SearchResult result = new SearchResult();
        SearchHits hits = response.getHits();
        // 1 返回的所有查询到的商品
        SearchHit[] searchHits = hits.getHits();
        List<SkuEsModel> esModelList = new ArrayList<>();
        if (ArrayUtil.isNotEmpty(searchHits)) {
            esModelList = Arrays.stream(searchHits).map(hit -> {
                SkuEsModel skuEsModel = JSONUtil.toBean(hit.getSourceAsString(), SkuEsModel.class);
                // 设置高亮信息
                if (StringUtils.hasText(param.getKeyword())) {
                    // 获取高亮字段
                    Map<String, HighlightField> highlightFields = hit.getHighlightFields();
                    HighlightField skuTitle = highlightFields.get("skuTitle");
                    String skuTitleValue = skuTitle.getFragments()[0].string();
                    skuEsModel.setSkuTitle(skuTitleValue);
                }
                return skuEsModel;
            }).collect(Collectors.toList());
        }
        result.setProducts(esModelList);

        // 聚合结果中获取
        // 2 当前所有商品涉及到的所有属性信息
        ParsedNested attr_agg = response.getAggregations().get("attr_agg");
        ParsedLongTerms attr_id_agg = attr_agg.getAggregations().get("attr_id_agg");
        List<SearchResult.AttrVo> attrVoList = attr_id_agg.getBuckets().stream().map(bucket -> {
            SearchResult.AttrVo attrVo = new SearchResult.AttrVo();
            // 获取属性 id
            String attrId = bucket.getKeyAsString();
            attrVo.setAttrId(Long.parseLong(attrId));
            // 获取属性名
            ParsedStringTerms attr_name_agg = bucket.getAggregations().get("attr_name_agg");
            String attrName = attr_name_agg.getBuckets().get(0).getKeyAsString();
            attrVo.setAttrName(attrName);
            // 获取属性值
            ParsedStringTerms attr_value_agg = bucket.getAggregations().get("attr_value_agg");
            List<String> attrValues = attr_value_agg.getBuckets().stream().map(MultiBucketsAggregation.Bucket::getKeyAsString).collect(Collectors.toList());
            attrVo.setAttrValues(attrValues);
            return attrVo;
        }).collect(Collectors.toList());
        result.setAttrs(attrVoList);

        // 3 当前所有商品涉及到的所有分类信息
        ParsedLongTerms catalog_agg = response.getAggregations().get("catalog_agg");
        List<SearchResult.CatalogVo> catalogVoList = catalog_agg.getBuckets().stream().map(bucket -> {
            SearchResult.CatalogVo catalogVo = new SearchResult.CatalogVo();
            // 获取分类 id
            String catalogId = bucket.getKeyAsString();
            catalogVo.setCatalogId(Long.parseLong(catalogId));
            // 获取分类名称
            ParsedStringTerms catalog_name_agg = bucket.getAggregations().get("catalog_name_agg");
            String catalogName = catalog_name_agg.getBuckets().get(0).getKeyAsString();
            catalogVo.setCatalogName(catalogName);
            return catalogVo;
        }).collect(Collectors.toList());
        result.setCatalogs(catalogVoList);

        // 4 当前所有商品涉及到的所有品牌信息
        ParsedLongTerms brand_agg = response.getAggregations().get("brand_agg");
        List<SearchResult.BrandVo> brandVoList = brand_agg.getBuckets().stream().map(bucket -> {
            SearchResult.BrandVo brandVo = new SearchResult.BrandVo();
            // 获取品牌 id
            String brandId = bucket.getKeyAsString();
            brandVo.setBrandId(Long.parseLong(brandId));
            // 获取品牌名称
            ParsedStringTerms brand_name_agg = bucket.getAggregations().get("brand_name_agg");
            String brandName = brand_name_agg.getBuckets().get(0).getKeyAsString();
            brandVo.setBrandName(brandName);
            // 获取品牌图片
            ParsedStringTerms brand_img_agg = bucket.getAggregations().get("brand_img_agg");
            String brandImg = brand_img_agg.getBuckets().get(0).getKeyAsString();
            brandVo.setBrandImg(brandImg);
            return brandVo;
        }).collect(Collectors.toList());
        result.setBrands(brandVoList);

        // 5 分页信息
        // 5.1 总记录数
        result.setTotal(hits.getTotalHits().value);
        // 5.2 总页码，计算得到
        int totalPages = (int) hits.getTotalHits().value % SearchConstant.PRODUCT_PAGE_SIZE == 0 ?
                (int) hits.getTotalHits().value / SearchConstant.PRODUCT_PAGE_SIZE :
                (int) hits.getTotalHits().value / SearchConstant.PRODUCT_PAGE_SIZE + 1;
        result.setTotalPages(totalPages);
        // 5.3 当前页码
        result.setPageNum(param.getPageNum());
        // 5.4 设置所有要显示的页码
        List<Integer> pageNavs = new ArrayList<>();
        for (int i = 1; i <= totalPages; i++) {
            pageNavs.add(i);
        }
        result.setPageNavs(pageNavs);

        // 6 构建面包屑导航
        // 6.1 属性面包屑导航
        if (CollectionUtils.isNotEmpty(param.getAttrs())) {
            List<SearchResult.NavVo> navs = param.getAttrs().stream().map(attr -> {
                // attrs: attr1=6_8G:16G&attr2=13_14寸:15.6寸
                SearchResult.NavVo navVo = new SearchResult.NavVo();
                String[] s = attr.split("_");
                // 设置导航的属性值
                navVo.setNavValue(s[1]);
                // 将作为检索条件的属性（添加到面包屑导航的属性）的 ID 保存到集合中
                result.getAttrIds().add(Long.parseLong(s[0]));
                // 设置导航的属性名，需要远程调用 product 服务，根据属性 id 查询属性名
                R r = productFeignService.getAttrInfo(Long.parseLong(s[0]));
                if (r.getCode() == 0) {
                    Object obj = r.get("attr");
                    if (obj != null) {
                        AttrResponseVo vo = JSONUtil.toBean(JSONUtil.toJsonStr(obj), AttrResponseVo.class);
                        navVo.setNavName(vo.getAttrName());
                        /*Map map = JSONUtil.toBean(JSONUtil.toJsonStr(obj), Map.class);
                        navVo.setNavName((String) map.get("attrName"));*/
                    }
                } else {
                    navVo.setNavName(s[0]);
                }
                // 设置取消面包屑导航后，跳转的url
                String replace = replaceQueryString(param, attr, "attrs");
                navVo.setLink("http://search.gulimall.com/list.html?" + replace);

                return navVo;
            }).collect(Collectors.toList());
            result.setNavs(navs);
        }

        // 6.2 品牌面包屑导航
        if (CollectionUtil.isNotEmpty(param.getBrandId())) {
            List<SearchResult.NavVo> navs = result.getNavs();
            SearchResult.NavVo navVo = new SearchResult.NavVo();
            navVo.setNavName("品牌");
            // 获取品牌信息，远程调用 product 服务
            R r = productFeignService.getBrandsInfo(param.getBrandId());
            if (r.getCode() == 0) {
                Object obj = r.get("brands");
                List<BrandVo> brandVos = JSONUtil.toList(JSONUtil.toJsonStr(obj), BrandVo.class);
                if (CollectionUtil.isNotEmpty(brandVos)) {
                    StringBuffer sb = new StringBuffer();
                    String replace = "";
                    for (BrandVo brandVo : brandVos) {
                        sb.append(brandVo.getName() + ";");
                        replace = replaceQueryString(param, brandVo.getBrandId() + "", "brandId");
                    }
                    navVo.setNavValue(sb.toString());
                    navVo.setLink("http://search.gulimall.com/list.html?" + replace);
                }
            }
            navs.add(navVo);
        }
        return result;
    }

    /**
     * 替换请求路径中的查询参数
     * @param param 查询参数
     * @param value 请求请求中待替换的参数值
     * @param key 请求请求中待替换的参数名
     * @return
     */
    private static String replaceQueryString(SearchParam param, String value, String key) {
        String queryString = param.get_queryString();
        // 请求路径中去除当前属性对应的请求参数
        String encode = null;
        try {
            encode = URLEncoder.encode(value, "UTF-8");
            // 替换空格，浏览器会把空格编码为 +，这里替换为 %20
            encode = encode.replace("+", "%20");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        String replace = queryString.replace("&" + key + "=" + encode, "");
        return replace;
    }
}
